namespace PugLib
{
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Drawing.Imaging;

    /// <summary>
    /// Identicon rendering class
    /// Version 1.3
    /// </summary>
    /// <author>Jeff Atwood http://www.codinghorror.com/, Jon Galloway http://weblogs.asp.net/jgalloway/</author>
    /// <remarks>
    /// Based on Don Park's Identicons 1.2 Java Code
    /// http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released
    /// </remarks>
    public class IdenticonRenderer
    {
        // Each "patch" in an Identicon is a polygon created from a list of vertices on a 5 by 5 grid.
        // Vertices are numbered from 0 to 24, starting from top-left corner of
        // the grid, moving left to right and top to bottom.
        private const int DEFAULT_PATCH_SIZE = 128;
        private const int MAX_SIZE = 1024;
        private const int MIN_SIZE = 16;
        private const int PATCH_CELLS = 4;
        private const int PATCH_GRIDS = PATCH_CELLS + 1;
        private const byte PATCH_INVERTED = 2;
        private const byte PATCH_SYMMETRIC = 1;
        private static readonly int[] centerPatchTypes = new[] { 0, 4, 8, 15 };

        private static readonly byte[] patch0 = new byte[] { 0, 4, 24, 20, 0 };
        private static readonly byte[] patch1 = new byte[] { 0, 4, 20, 0 };
        private static readonly byte[] patch10 = new byte[] { 0, 2, 12, 10, 0 };
        private static readonly byte[] patch11 = new byte[] { 10, 14, 22, 10 };
        private static readonly byte[] patch12 = new byte[] { 20, 12, 24, 20 };
        private static readonly byte[] patch13 = new byte[] { 10, 2, 12, 10 };
        private static readonly byte[] patch14 = new byte[] { 0, 2, 10, 0 };
        private static readonly byte[] patch2 = new byte[] { 2, 24, 20, 2 };
        private static readonly byte[] patch3 = new byte[] { 0, 2, 22, 20, 0 };
        private static readonly byte[] patch4 = new byte[] { 2, 14, 22, 10, 2 };
        private static readonly byte[] patch5 = new byte[] { 0, 14, 24, 22, 0 };
        private static readonly byte[] patch6 = new byte[] { 2, 24, 22, 13, 11, 22, 20, 2 };
        private static readonly byte[] patch7 = new byte[] { 0, 14, 22, 0 };
        private static readonly byte[] patch8 = new byte[] { 6, 8, 18, 16, 6 };
        private static readonly byte[] patch9 = new byte[] { 4, 20, 10, 12, 2, 4 };

        private static readonly byte[] patchFlags = new byte[]
                                                        {
                                                            PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0,
                                                            PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0,
                                                            (PATCH_SYMMETRIC + PATCH_INVERTED)
                                                        };

        private static readonly byte[][] patchTypes = new[]
                                                          {
                                                              patch0, patch1, patch2, patch3, patch4, patch5, patch6,
                                                              patch7, patch8, patch9, patch10, patch11, patch12, patch13,
                                                              patch14, patch0
                                                          };

        private int _patchOffset; // used to center patch shape at origin because shape rotation works correctly.

        private GraphicsPath[] _patchShapes;
        private int _patchSize;

        public IdenticonRenderer()
        {
            PatchSize = DEFAULT_PATCH_SIZE;
        }

        public IdenticonRenderer(int patchSize)
        {
            PatchSize = patchSize;
        }

        /// <summary>
        /// The size in pixels at which each patch will be rendered interally before they
        /// are scaled down to the requested identicon size. Default size is 20 pixels
        /// which means, for 9-block identicon, a 60x60 image will be rendered and
        /// scaled down.
        /// </summary>
        public int PatchSize
        {
            get { return _patchSize; }

            set
            {
                _patchSize = value;
                _patchOffset = _patchSize / 2; // used to center patch shape at origin.
                int scale = _patchSize / PATCH_CELLS;
                _patchShapes = new GraphicsPath[patchTypes.Length];
                for (int i = 0; i < patchTypes.Length; i++)
                {
                    GraphicsPath patch = new GraphicsPath();
                    byte[] patchVertices = patchTypes[i];
                    for (int j = 0; j < patchVertices.Length; j++)
                    {
                        int v = patchVertices[j];
                        int vx = (v % PATCH_GRIDS * scale) - _patchOffset;
                        int vy = (v / PATCH_GRIDS * scale) - _patchOffset;
                        AddPointToGraphicsPath(patch, vx, vy);
                    }
                    _patchShapes[i] = patch;
                }
            }
        }

        /// <summary>
        /// Adds the X and Y coordinates to the current graphics path.
        /// </summary>
        /// <param name="gp"> The current Graphics path</param>
        /// <param name="x">The x coordinate to be added</param>
        /// <param name="y">The y coordinate to be added</param>
        private static void AddPointToGraphicsPath(GraphicsPath gp, int x, int y)
        {
            PointF[] tempPointArray = new PointF[gp.PointCount + 1];
            byte[] tempPointTypeArray = new byte[gp.PointCount + 1];

            if (gp.PointCount == 0)
            {
                tempPointArray[0] = new PointF(x, y);
                GraphicsPath tempGraphicsPath = new GraphicsPath(tempPointArray, new[] { (byte)PathPointType.Start });
                gp.AddPath(tempGraphicsPath, false);
            }
            else
            {
                gp.PathPoints.CopyTo(tempPointArray, 0);
                tempPointArray[gp.PointCount] = new Point(x, y);

                gp.PathTypes.CopyTo(tempPointTypeArray, 0);
                tempPointTypeArray[gp.PointCount] = (byte)PathPointType.Line;

                GraphicsPath tempGraphics = new GraphicsPath(tempPointArray, tempPointTypeArray);
                gp.Reset();
                gp.AddPath(tempGraphics, false);
                gp.CloseFigure();
            }
        }


        public Bitmap Render(int code)
        {
            return Render(code, PatchSize);
        }


        /// <summary>
        /// Returns rendered identicon bitmap for a given identicon code.
        /// </summary>
        /// <param name="code">Identicon code</param>
        /// <param name="size">desired image size</param>
        public Bitmap Render(int code, int size)
        {
            if (size > MAX_SIZE)
            {
                size = MAX_SIZE;
            }
            if (size < MIN_SIZE)
            {
                size = MIN_SIZE;
            }

            // decode the code into parts:            
            // bit 0-1: middle patch type
            int centerType = centerPatchTypes[code & 0x3];
            // bit 2: middle invert
            bool centerInvert = ((code >> 2) & 0x1) != 0;
            // bit 3-6: corner patch type
            int cornerType = (code >> 3) & 0x0f;
            // bit 7: corner invert
            bool cornerInvert = ((code >> 7) & 0x1) != 0;
            // bit 8-9: corner turns
            int cornerTurn = (code >> 8) & 0x3;
            // bit 10-13: side patch type
            int sideType = (code >> 10) & 0x0f;
            // bit 14: side invert
            bool sideInvert = ((code >> 14) & 0x1) != 0;
            // bit 15: corner turns
            int sideTurn = (code >> 15) & 0x3;
            // bit 16-20: blue color component
            int blue = (code >> 16) & 0x01f;
            // bit 21-26: green color component
            int green = (code >> 21) & 0x01f;
            // bit 27-31: red color component
            int red = (code >> 27) & 0x01f;

            // color components are used at top of the range for color difference
            // use white background for now. TODO: support transparency.
            Color foreColor = Color.FromArgb(red << 3, green << 3, blue << 3);
            Color backColor = Color.White;

            // outline shapes with a noticeable color (complementary will do) if
            // shape color and background color are too similar (measured by color
            // distance).
            Color strokeColor = Color.Empty;
            if (ColorDistance(foreColor, backColor) < 32f)
            {
                strokeColor = ComplementaryColor(foreColor);
            }

            // render at larger source size (to be scaled down later)
            int sourceSize = _patchSize * 3;
            Bitmap b;

            using (Bitmap sourceImage = new Bitmap(sourceSize, sourceSize, PixelFormat.Format32bppRgb))
            {
                using (Graphics g = Graphics.FromImage(sourceImage))
                {
                    // center patch
                    drawPatch(g, _patchSize, _patchSize, centerType, 0, centerInvert, foreColor, backColor, strokeColor);

                    // side patch (top)
                    drawPatch(g, _patchSize, 0, sideType, sideTurn++, sideInvert, foreColor, backColor, strokeColor);
                    // side patch (right)
                    drawPatch(g, _patchSize * 2, _patchSize, sideType, sideTurn++, sideInvert, foreColor, backColor,
                              strokeColor);
                    // side patch (bottom)
                    drawPatch(g, _patchSize, _patchSize * 2, sideType, sideTurn++, sideInvert, foreColor, backColor,
                              strokeColor);
                    // side patch (left)
                    sideTurn++;
                    drawPatch(g, 0, _patchSize, sideType, sideTurn, sideInvert, foreColor, backColor, strokeColor);

                    // corner patch (top left)
                    drawPatch(g, 0, 0, cornerType, cornerTurn++, cornerInvert, foreColor, backColor, strokeColor);
                    // corner patch (top right)
                    drawPatch(g, _patchSize * 2, 0, cornerType, cornerTurn++, cornerInvert, foreColor, backColor,
                              strokeColor);
                    // corner patch (bottom right)
                    drawPatch(g, _patchSize * 2, _patchSize * 2, cornerType, cornerTurn++, cornerInvert, foreColor,
                              backColor, strokeColor);
                    // corner patch (bottom left)
                    cornerTurn++;
                    drawPatch(g, 0, _patchSize * 2, cornerType, cornerTurn, cornerInvert, foreColor, backColor,
                              strokeColor);
                }

                // scale source image to target size with bicubic smoothing
                b = new Bitmap(size, size, PixelFormat.Format32bppRgb);
                using (Graphics g = Graphics.FromImage(b))
                {
                    int fudge = (int)(size * 0.016); // this is necessary to prevent scaling artifacts at larger sizes
                    g.DrawImage(sourceImage, 0, 0, size + fudge, size + fudge);
                }
            }

            return b;
        }

        private void drawPatch(Graphics g, int x, int y, int patch, int turn, bool invert, Color fore, Color back,
                               Color stroke)
        {
            patch %= patchTypes.Length;
            turn %= 4;
            if ((patchFlags[patch] & PATCH_INVERTED) != 0)
            {
                invert = !invert;
            }

            // paint the background
            g.FillRegion(new SolidBrush(invert ? fore : back), new Region(new Rectangle(x, y, _patchSize, _patchSize)));

            // offset and rotate coordinate space by patch position (x, y) and
            // 'turn' before rendering patch shape
            Matrix m = g.Transform;
            g.TranslateTransform((x + _patchOffset), (y + _patchOffset));
            g.RotateTransform(turn * 90);

            // if stroke color was specified, apply stroke
            // stroke color should be specified if fore color is too close to the back color.
            if (!stroke.IsEmpty)
            {
                g.DrawPath(new Pen(stroke), _patchShapes[patch]);
            }

            // render rotated patch using fore color (back color if inverted)
            g.FillPath(new SolidBrush(invert ? back : fore), _patchShapes[patch]);

            // restore previous rotation
            g.Transform = m;
        }

        /// <summary>Returns distance between two colors</summary>		
        private static float ColorDistance(Color c1, Color c2)
        {
            float dx = (c1.R - c2.R);
            float dy = (c1.G - c2.G);
            float dz = (c1.B - c2.B);
            return (float)System.Math.Sqrt(dx * dx + dy * dy + dz * dz);
        }

        /// <summary>Returns complementary color</summary>
        private static Color ComplementaryColor(Color c)
        {
            return Color.FromArgb(c.ToArgb() ^ 0x00FFFFFF);
        }
    }
}